Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.

...powered by www.netzwerkartist.de...

 << zurück
Visual C# 2005 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual C# 2005

Visual C# 2005
1.320 S., mit 2 CDs, 59,90 Euro
Galileo Computing
ISBN 3-89842-586-X
gp Kapitel 3 Grundlagen der Sprache C#
  gp 3.1 Konsolenanwendungen
    gp 3.1.1 Allgemeine Anmerkungen
    gp 3.1.2 Ein erstes Konsolenprogramm
  gp 3.2 Grundlagen der C#-Syntax
    gp 3.2.1 Kennzeichnen, dass eine Anweisung abgeschlossen ist
    gp 3.2.2 Anweisungs- und Gliederungsblöcke
    gp 3.2.3 Kommentare
    gp 3.2.4 Die Groß- und Kleinschreibung
    gp 3.2.5 Die Struktur einer Konsolenanwendung
  gp 3.3 Variablen und Datentypen
    gp 3.3.1 Variablendeklaration
    gp 3.3.2 Der Variablenbezeichner
    gp 3.3.3 Der Zugriff auf eine Variable
    gp 3.3.4 Ein- und Ausgabemethoden der Klasse »Console«
    gp 3.3.5 Die einfachen Datentypen
    gp 3.3.6 Typkonvertierung
  gp 3.4 Operatoren
    gp 3.4.1 Arithmetische Operatoren
    gp 3.4.2 Vergleichsoperatoren
    gp 3.4.3 Logische Operatoren
    gp 3.4.4 Bitweise Operatoren
    gp 3.4.5 Zuweisungsoperatoren
    gp 3.4.6 Stringverkettung
    gp 3.4.7 Sonstige Operatoren
    gp 3.4.8 Operator-Vorrangregeln
  gp 3.5 Datenfelder (Arrays)
    gp 3.5.1 Die Deklaration und Initialisierung eines Arrays
    gp 3.5.2 Der Zugriff auf die Array-Elemente
    gp 3.5.3 Speicherabbild eines Arrays
    gp 3.5.4 Mehrdimensionale Arrays
    gp 3.5.5 Festlegen der Array-Größe zur Laufzeit
    gp 3.5.6 Bestimmung der Array-Obergrenze
    gp 3.5.7 Die Gesamtanzahl der Array-Elemente
    gp 3.5.8 Verzweigte Arrays
  gp 3.6 Kontrollstrukturen
    gp 3.6.1 Die »if«-Anweisung
    gp 3.6.2 Das »switch«-Statement
  gp 3.7 Programmschleifen
    gp 3.7.1 Die »for«-Schleife
    gp 3.7.2 Die »foreach«-Schleife
    gp 3.7.3 Die »do«- und die »while«-Schleife


Galileo Computing

3.3 Variablen und Datentypen  downtop

Dateninformationen bilden die Grundlage der Datenverarbeitung und hauchen einem Programm Leben ein: Daten können anwendungsspezifisch sein, den Zustand von Objekten beschreiben, Informationen aus Datenbanken repräsentieren oder auch nur eine Netzwerkadresse. Daten bilden also gemeinhin die Basis der Gesamtfunktionalität einer Anwendung.


Galileo Computing

3.3.1 Variablendeklaratiodowntop

Praktisch jedes Programm benötigt Daten, um bestimmte Aufgaben zu erfüllen. Daten werden in Variablen vorgehalten. Dabei steht eine Variable für eine Adresse im Hauptspeicher des Rechners. Ausgehend von dieser Adresse wird eine bestimmte Anzahl von Bytes reserviert – entsprechend dem Typ des Werts. Das, was eine Variable repräsentiert, kann vielfältiger Art sein: eine einfache Zahl, eine große Fließkommazahl, ein einzelnes Zeichen, eine Zeichenkette, eine Datums- oder Zeitangabe, aber auch die Referenz auf die Startadresse eines Objekts.

Der Variablenname, auch Bezeichner genannt, dient dazu, die Speicheradresse im Programmcode mit einem einfach zu merkenden Namen anzusprechen. Er ist also vom Wesen her nichts anderes als ein Synonym oder Platzhalter einer bestimmten Speicherlokalität.

Variablen müssen deklariert werden. Unter einer Variablendeklaration wird die Bekanntgabe des Namens der Variablen sowie des von ihr repräsentierten Datentyps verstanden. Die Deklaration muss vor der ersten Wertzuweisung an die Variable erfolgen. Dabei wird zuerst der Datentyp angegeben, dahinter der Variablenname. Abgeschlossen wird die Deklaration mit einem Semikolon. Damit lautet die allgemeine Syntax:


// Syntax: Variablendeklaration
Datentyp Bezeichner;

Beispielsweise könnte eine zulässige Deklaration wie folgt aussehen:


int intVar;

Damit wird dem Compiler mitgeteilt, dass der Bezeichner intVar für einen Wert steht, der vom Typ einer Ganzzahl, genauer gesagt vom Typ int (Integer) ist. Mit


intVar = 1000;

wird dieser Variablen ein gültiger Wert zugewiesen. Man spricht dann auch von der Initialisierung der Variablen.

Versuchen Sie, auf eine nicht deklarierte Variable zuzugreifen, wird der C#-Compiler einen Fehler melden. Ebenso falsch ist es, den Inhalt einer nicht initialisierten Variablen auswerten zu wollen.


Variablen, die innerhalb einer Prozedur (= Methode) wie beispielsweise Main deklariert sind, gelten noch nicht als initialisiert. Sie enthalten keinen gültigen Wert, auch nicht 0. Daher kann ihr Inhalt auch nicht ausgewertet werden.

Deklaration und Initialisierung können auch in einer einzigen Anweisung erfolgen:


int intVar = 0;

Auf diese Weise vermeiden Sie eine uninitialisierte Variable.

Müssen Sie mehrere Variablen gleichen Typs deklarieren, können Sie die Bezeichner, getrennt durch ein Komma, hintereinander angeben:


int a, b, c;

Sie können dann auch eine oder mehrere Variablen sofort initialisieren:


int a, b = 9, c = 12;


Galileo Computing

3.3.2 Der Variablenbezeichner  downtop

Ein Variablenname kann nahezu beliebig festgelegt werden, unterliegt aber besonderen Reglementierungen:

gp  Ein Bezeichner darf sich nur aus alphanumerischen Zeichen und dem Unterstrich zusammensetzen. Leerzeichen und andere Sonderzeichen wie beispielsweise #, §, $ usw. sind nicht zugelassen.
gp  Ein Bezeichner muss mit einem Buchstaben oder dem Unterstrich anfangen.
gp  Ein allein stehender Unterstrich als Variablenname ist nicht zulässig.
gp  Der Bezeichner muss eindeutig sein. Er darf nicht gleichlautend mit einem Schlüsselwort, einer Prozedur, einer Klasse oder einem Objektnamen sein.
gp  Zur Verdeutlichung dieser Regeln einige Beispiele für korrekte und falsche Variablenbezeichner:

// korrekte Variablendeklarationen
long lngMyVar;
byte bResult_12;
int intCarColor;
// fehlerhafte Variablendeklarationen
int 34M;
string strMessage Text;
long longSalary%Tom;

Noch ein Hinweis zur Namensvergabe. Wählen Sie grundsätzlich beschreibende Namen, damit Ihr Code später besser lesbar wird. Einfache Bezeichner wie x oder y usw. sind wenig aussagefähig. Besser wäre eine Wahl wie intFarbe, dblGehalt, strVorname usw. Nur den Zählervariablen von Schleifen werden meistens Kurznamen gegeben.

Die Bezeichner von Variablen sollten mit einem Kleinbuchstaben anfangen – im Gegensatz zu den Bezeichnern von Methoden und Objekten. Wenn Sie es besonders gut machen wollen, können Sie auch ein Präfix benutzen, aus dem hervorgeht, welcher Datentyp von der Variablen beschrieben wird, z.B.:


int intVar;
string strText;

In der Tabelle 3.1 finden Sie einige typbeschreibende Präfixvorschläge.


Tabelle 3.1   Präfixvorschläge zur Variablenbezeichnung

Datentyp Präfix
short srt
int int
long lng
float flt
double dbl
decimal dec
char chr
string str
bool bol
byte byt


Galileo Computing

3.3.3 Der Zugriff auf eine Variable  downtop

Wir wollen uns jetzt noch ansehen, wie wir uns den Inhalt einer Variablen an der Konsole ausgeben lassen können. Wir deklarieren dazu eine Variable vom Typ long und weisen ihr einen Wert zu, den wir danach an der Konsole ausgeben lassen.


static void Main(string[] args)
{
  long lngVar = 4711;
  Console.WriteLine("lngVar = {0}", lngVar);
  Console.ReadLine();
}

Deklaration und Initialisierung bieten keine Neuigkeiten, im Gegensatz zu der Anweisung, die eine Ausgabe an der Konsole bewirkt:


Console.WriteLine("lngVar = {0}",lngVar);

Die Ausgabe im Befehlsfenster wird lauten:


lngVar = 4711

Sie haben bereits gesehen, mit Console.WriteLine wird eine einfache Konsolenausgabe codiert. WriteLine ist eine Methode, die in der Klasse Console definiert ist. Jetzt fehlt noch die genaue Erklärung der verwendeten Syntax.


Galileo Computing

3.3.4 Ein- und Ausgabemethoden der Klasse »Console«  downtop

Es bleibt uns nichts anderes übrig, als an dieser Stelle schon einen kleinen Ausflug in die Welt der Klassen und Objekte zu unternehmen. Obwohl wir uns erst ab Kapitel 4 intensiv mit diesen Themen auseinander setzen werden, kommen wir in diesem Kapitel nicht daran vorbei, weil wir immer wieder mit den Methoden verschiedener Klassen arbeiten werden. Es handelt sich dabei meist um Methoden, um an der Eingabekonsole Ein- und Ausgabeoperationen durchzuführen: Write und WriteLine sowie Read und ReadLine.

Die Methoden »WriteLine«, »ReadLine«, »Write« und »Read«

Die Klasse Console ermöglicht den Zugriff auf die Standardausgabeschnittstelle über die beiden Methoden Write und WriteLine. Der Begriff »Ausgabeschnittstelle« mag im ersten Moment ein wenig verwirren, aber tatsächlich wird darunter die Anzeige an der Konsole verstanden.

WriteLine und Write unterscheiden sich dahingehend, dass die erstgenannte Methode dem Ausgabestring automatisch einen Zeilenumbruch anhängt und den Cursor in die folgende Ausgabezeile setzt. Nach dem Aufruf der Methode Write verbleibt der Eingabecursor weiterhin in der aktuellen Ausgabezeile.


Ein Zeilenumbruch ist die Kombination aus Wagenrücklauf und Zeilenvorschub und entspricht den ASCII-Werten 13 und 10. An der Tatstatur wird der Zeilenumbruch durch Drücken der mit (Enter) bzw. (Return) beschrifteten Eingabetaste erreicht.

Beide Methoden sind auf vielfältige Weise einsetzbar. Denn unabhängig vom zugrunde liegenden Datentyp werden beide Ausgabemethoden ohne zu murren die gewünschten Daten im Konsolenfenster anzeigen.

Wollen wir die Methode eines Objekts aufrufen, geben wir den Objektnamen an und von diesem durch einen Punkt getrennt – die so genannte Punktnotation – den Namen der Methode. Hinter dem Methodennamen schließt sich ein Klammerpaar an. Allgemein lautet die Syntax also:


// die Punktnotation
Objektname.Methodenname();

Sie können sich mit dieser Syntax durchaus schon vertraut machen, sie wird Ihnen ab sofort überall begegnen, da sie elementar in objektorientiertem Programmcode ist.

Das runde Klammerpaar hinter der Read- bzw. ReadLine-Methode bleibt immer leer, bei den Methoden Write und WriteLine werden innerhalb der Klammern die auszugebenden Daten einschließlich ihres Ausgabeformats beschrieben. Allerdings dürfen auch bei den beiden letztgenannten Methoden die Klammern leer bleiben.

Im einfachsten Fall kann einer der beiden Ausgabemethoden eine Zeichenfolge in Anführungsstrichen übergeben werden:


Console.WriteLine("C# macht Spass.");

Formatausdrücke in den Methoden »Write« und »WriteLine«

Damit sind die Möglichkeiten der Write-/WriteLine-Methoden noch lange nicht erschöpft. Die flexiblen Formatierungsmöglichkeiten erlauben die Ausgabe von Daten an beliebigen Positionen innerhalb der Ausgabezeichenfolge. Dazu dient ein Platzhalter, der auch als Formatausdruck bezeichnet wird. Dieser ist an den geschweiften Klammern zu erkennen und enthält zumindest eine Zahl. Hinter der auszugebenden Zeichenfolge werden, durch ein Komma getrennt, die Informationen übergeben, was anstelle des Formatausdrucks auszugeben ist. Sehen wir uns dazu ein Beispiel an:


string strText1 = "C#";
string strText2 = "Spass";
Console.Write("{0} macht {1}.", strText1, strText2);

Es sind die beiden Variablen strText1 und strText2 vom Typ string deklariert, die mit einer in Anführungsstrichen gesetzten Zeichenfolge initialisiert werden.

Die auszugebende Zeichenfolge wird in Anführungsstriche gesetzt. Getrennt durch Kommata werden dahinter die beiden Variablen strText1 und strText2 bekannt gegeben. Der Inhalt der zuerst genannten Variablen strText1 ersetzt den Formatausdruck {0} innerhalb der Ausgabezeichenfolge, die zweite Variable strText2 den Formatausdruck {1}. Entscheidend ist, dass dem ersten Parameter die Zahl 0 zugeordnet wird, dem zweiten die Zahl 1 usw. Die Konsolenausgabe lautet:


C# macht Spass.

Innerhalb des Ausgabestrings müssen die anzuzeigenden Listenelemente nicht der Reihenfolge nach durchlaufen werden, man kann sie beliebig ansprechen oder sogar einfach ungenutzt lassen. Die Anweisung


Console.Write("{1} macht {0}.", strText1, strText2);

würde demnach zu der folgenden Ausgabe führen:


Spass macht C#.

Der Formatausdruck {} dient nicht nur der eindeutigen Bestimmung des Elements, er ermöglicht auch eine weitergehende Einflussnahme auf die Ausgabe. Soll der einzusetzende Wert eine bestimmte Breite einnehmen, gilt die syntaktische Variante:

{N, M}

Dabei gilt das Folgende:

gp  N ist ein nullbasierter Zähler.
gp  M gibt die Breite der Ausgabe an.

Unbesetzte Plätze werden durch eine entsprechende Anzahl von Leerzeichen aufgefüllt. Sehen wir uns dazu ein Codefragment an:


int intVar = 10;
Console.WriteLine("Ich kaufe {0,3} Eier", intVar);
Console.WriteLine("Ich kaufe {0,10} Eier", intVar);

Die Ausgabe lautet hier:


Ich kaufe  10 Eier
Ich kaufe         10 Eier 

Die erste Ausgabe hat eine Gesamtbreite von drei Zeichen, die Zahl selbst ist allerdings nur zwei Ziffern breit. Daher wird vor der Zahl ein Leerzeichen gesetzt. Da für die Breite der zweiten Ausgabe zehn Zeichen vorgeschrieben sind, werden links von der Zahl acht Leerstellen eingefügt.

Die Breite darf auch eine negative Zahl sein. Die Ausgabe erfolgt dann allerdings linksbündig, daran schließen sich die Leerstellen an.

Sie können den Formatausdruck so spezifizieren, dass nummerische Ausgabedaten eine bestimmte Formatierung annehmen. Das führt uns zu der vollständigen Syntax des Formatausdrucks:


// Syntax: Formatausdruck
{N [,M ][: Format]}

Format spezifiziert, wie die Daten angezeigt werden. In der Tabelle 3.2 werden die möglichen Optionen aufgelistet.


Tabelle 3.2   Formatangaben der Formatausgabe

Formatangabe Beschreibung
C Zeigt die Zahl im lokalen Währungsformat an.
D Zeigt die Zahl als dezimalen Integer an.
E Zeigt die Zahl im wissenschaftlichen Format an (Exponentialschreibweise).
F Zeigt die Zahl im Festpunktformat an.
G Eine nummerische Zahl wird entweder im Festpunkt- oder im wissenschaftlichen Format angezeigt. Zur Anzeige kommt das »kompakteste« Format.
N Zeigt eine nummerische Zahl einschließlich Kommaseparatoren an.
P Zeigt die nummerische Zahl als Prozentzahl an.
X Die Anzeige erfolgt in Hexadezimalnotation.

Allen Formatangaben kann eine Zahl angehängt werden, aus der die Anzahl der signifikanten Stellen hervorgeht. Nachfolgend sollen einige Beispiele den Einsatz der Formatangaben demonstrieren:


int intVar = 4711;
Console.WriteLine("intVar={0:C}", intVar);
// Ausgabe: intVar=4.711,00 DM
Console.WriteLine("intVar={0:E}", intVar);
// Ausgabe: intVar=4,711000E+003
Console.WriteLine("intVar={0:E2}", intVar);
// Ausgabe: intVar=4,71E+003
int i = 225;
Console.WriteLine("i={0:X}", i);
// Ausgabe: i=E1
float fltVar = 0.2512F;
Console.WriteLine("fltVar={0,10:G}", fltVar);
// Ausgabe: fltVar=    0,2512
Console.WriteLine("fltVar={0:P4}", fltVar);
// Ausgabe: fltVar=25,1200  %

Escape-Zeichen

Ähnlich wie in den Sprachen C++ und Java stellt C# eine Reihe von Escape-Sequenzen zur Verfügung, die dann verwendet werden, wenn Sonderzeichen innerhalb einer Zeichenfolge ausgegeben werden sollen. Beispielsweise kann man mit dem Zeichen \n einen Zeilenumbruch erzwingen:


Console.Write("C#\nmacht\nSpass.");

An der Konsole wird dann


C#
macht
Spass.

angezeigt.


Tabelle 3.3   Die Escape-Zeichen

Escape-Zeichen Beschreibung
\' Fügt ein Hochkomma in die Zeichenfolge ein.
\'' Fügt Anführungsstriche ein.
\\ Fügt in die Zeichenfolge einen Backslash ein.
\a Löst einen Alarmton aus.
\b Führt zum Löschen des vorhergehenden Zeichens.
\f Löst einen Formularvorschub bei Druckern aus.
\n Löst einen Zeilenvorschub aus (entspricht der Funktionalität der (Enter)-Taste).
\r Führt zu einem Wagenrücklauf.
\t Führt auf dem Bildschirm zu einem Tabulatorsprung.
\u Fügt ein Unicode-Zeichen in die Zeichenfolge ein.
\v Fügt einen vertikalen Tabulator in eine Zeichenfolge ein.

Mit Escape-Sequenzen lässt sich die Ausgabe von Sonderzeichen sicherstellen. Es ist aber auch vorstellbar, dass Zeichen, die vom Compiler als Escape-Sequenz interpretiert werden, selbst Bestandteil der Zeichenfolge sind. Fügen Sie dazu nur noch einen weiteren Schrägstrich ein. Dazu ein kleines Beispiel. Angenommen, Sie möchten die Ausgabe


Hallo\nWelt

erzwingen. Sie müssten dann die folgende Anweisung codieren:


Console.WriteLine("Hallo\\nWelt");

Um die Interpretation als Escape-Sequenz für eine gegebene Zeichenfolge vollständig abzuschalten, wird vor der Zeichenfolge das Zeichen »@« gesetzt.


Console.Write(@"C#\nmacht\nSpass.");

Jetzt lautet die Konsolenausgabe:


C#\nmacht\nSpass.

Die Methoden »ReadLine« und »Read«

Die Methode ReadLine liest ein oder mehrere Zeichen aus dem Eingabestrom – in unserem Fall ist das die Tastatur. Die Bereitschaft der Methode, auf Zeichen zu warten, endet mit dem Zeilenumbruch, der jedoch selbst nicht zu den eingelesenen Daten gehört. Die eingelesene Zeichenfolge wird von der Methode als Zeichenfolge vom Typ string zurückgeliefert und kann somit einer string-Variablen zugewiesen werden.

string strEingabe = Console.ReadLine(); Console.WriteLine(strEingabe);

Wir haben bisher die ReadLine-Methode dazu benutzt, um die Konsole bis zum Drücken der (Enter)-Taste geöffnet zu halten. In diesem Fall war der Eingabestrom immer leer, der Rückgabewert wurde ignoriert und landete im Nirwana.

Werfen wir nun einen Blick auf die Read-Methode. Diese nimmt nur ein Zeichen aus dem Eingabestrom und gibt dessen ASCII-Wert zurück. Der Rückgabewert von Read ist daher keine Zeichenfolge, sondern eine Zahl vom Typ int.

Es gibt aber noch einen weiteren, nicht weniger wichtigen Unterschied zwischen Read und ReadLine: Die ReadLine-Methode liest eine ganze Zeile und benutzt den Zeilenumbruch dazu, das Ende der Eingabe zu erkennen. Danach wird der Zeilenumbruch dem Eingabestrom entnommen und gelöscht. Die Read-Methode arbeitet anders, denn der Zeilenumbruch wird nicht aus dem Eingabestrom geholt, sondern verbleibt dort und wird so lange gepuffert, bis er von einer anderen Anweisung gelöscht wird. Das kann wiederum nur die Methode ReadLine sein. Schauen Sie sich dazu das folgende Codefragment an:


static void Main(string[] args)
{
  int eingabe = Console.Read();  
  Console.WriteLine(eingabe);
  Console.ReadLine();
}

Nach dem Start des Programms wartet Read auf die Eingabe des Anwenders und erkennt am Zeilenumbruch das Eingabeende. Der Zeilenumbruch befindet sich weiterhin im Eingabestrom und harrt geduldig der kommenden Anweisungen. Die Anweisung in der letzten Zeile, die ReadLine-Methode, reagiert als Erstes wieder auf den Eingabestrom, erkennt darin den Zeilenumbruch und verarbeitet ihn. Das ist gleichzeitig auch das Signal, mit der nächsten Anweisung fortzufahren. Da aber das Ende der Main-Methode erreicht ist, schließt sich das Konsolenfenster sofort. Erst ein zweiter Aufruf von ReadLine würde den eigentlich angedachten Zweck erfüllen, nämlich das Fenster geöffnet zu halten und die Ausgabe der WriteLine-Methode auf unbestimmte Zeit anzuzeigen.

Wenn mehrere Zeichen an der Konsole eingegeben werden, kann Read nur das erste auswerten. Alle weiteren bleiben zusammen mit dem Zeilenumbruch im Eingabestrom. Mit


Console.WriteLine(Console.ReadLine);

könnten Sie sich diese anzeigen lassen.


Galileo Computing

3.3.5 Die einfachen Datentypen  downtop

Die .NET-Laufzeitumgebung verfolgt das Konzept der Objektorientierung nach strengen Maßstäben. Selbst einfache Datentypen werden als Objekte angesehen, die Methoden bereitstellen, um mit einer Variablen bestimmte Aktionen auszuführen. In der Tabelle 3.4 sind alle nativen Datentypen von C# zusammenfassend aufgeführt.


Tabelle 3.4   Die elementarsten Datentypen

.NET-Laufzeittyp C# -Alias CLS-kompatibel Wertebereich
Byte byte ja 0 ... 255
SByte sbyte nein –128 ... 127
Int16 short ja –215  . 215  –1
UInt16 ushort nein 0 . 65535
Int32 int ja –231  . 231  –1
UInt32 uint nein 0 ... 232   –1
Int64 long ja –263  . 263  –1
UInt64 ulong nein 0 . 264  –1
Single float ja 1,4 * 10–  45  bis 3,4 * 1038 
Double double ja 5,0 * 10–  324  bis 1,7 * 10308 
Decimal decimal ja +/–79E27 ohne Dezimalpunktangabe; +/–7.9E-29, falls 28 Stellen hinter dem Dezimalpunkt angegeben werden. Die kleinste darstellbare Zahl beträgt +/–1.0E-29.
Char char ja Unicode-Zeichen zwischen 0 und 65535
String string ja ca. 231   Unicode-Zeichen
Boolean bool ja true oder false
Object object ja Ein Variable vom Typ Object kann jeden anderen Datentypen enthalten, ist also universell.

In der ersten Spalte ist der Typbezeichner in der .NET-Klassenbibliothek angeführt, in der zweiten Spalte der C#-Alias, der bei der Deklaration einer Variablen dieses Typs angegeben werden kann.

Zu den Angaben in der dritten Spalte muss ich Ihnen eine Erklärung geben. .NET verfolgt nicht nur ein plattformunabhängiges Konzept, sondern auch ein sprachunabhängiges. Das bedeutet, dass eine Komponente, die in einer fiktiven .NET-Programmiersprache A geschrieben wird, auch den vollen Zugriff auf alle Features einer Komponente haben sollte, die in einer anderen .NET-Sprache, nennen wir sie hier der Einfachheit halber B, implementiert ist.

Das kann nur problemlos funktionieren, wenn sich beide auf einen gemeinsamen Nenner hinsichtlich der Sprachfeatures geeignet haben, zu denen auch die elementaren Datentypen zu rechnen sind. Dieser gemeinsame Nenner wird durch die Common Language Specification (CLS) vorgegeben. Das bedeutet, dass, wenn eine .NET-Anwendung CLS-konform codiert wird, eine Garantie damit verbunden ist, dass jeder andere .NET-Code Zugriff auf die (öffentlichen) Komponenten der CLS-konformen Anwendung hat – unabhängig davon, in welcher Sprache sie codiert ist.

Wie Sie sehen, sind nicht alle Datentypen der Tabelle 3.4 CLS-konform. Sie können diese zwar innerhalb Ihrer Anwendung problemlos einsetzen, aber in der öffentlichen Schnittstelle haben sie nichts zu suchen.

a
Achtung   Vielleicht können Sie an dieser Stelle mit dem Begriff der öffentlichen Schnittstelle noch nicht allzu viel anfangen. Am Ende des Kapitels 4 werden Sie aber wissen, was damit gemeint ist. Solange muss ich Sie noch vertrösten.

Wie der Tabelle zu entnehmen ist, basieren alle Typen auf einer Klassendefinition im .NET Framework. Das hat zur Folge, dass anstelle der Angabe des C#-Alias zur Typbeschreibung auch der .NET-Laufzeittyp genannt werden kann. Damit sind die beiden folgenden Deklarationen der Variablen intVar absolut gleichwertig:


int intVar;
Int32 intVar;

Wertetypen und Referenztypen

.NET unterscheidet zwei wesentliche Gruppen von Datentypen:

1.  Wertetypen: Zu dieser Gruppe werden die elementarsten Datentypen gezählt, z.B. Int16, Int32, Boolean, Double, usw. Das entscheidende Kriterium besteht darin, dass die zugrunde liegende Typdefinition von der Klasse ValueType abgeleitet ist.
2. Referenztypen: Zu dieser Gruppe werden alle Typen gezählt, deren Typdefinition nicht von ValueType abgeleitet ist. Das ist der überwiegende Teil aller Klassen der .NET-Bibliothek. Kennzeichnend für diese Gruppe ist, dass die Variable eines Referenztyps einen Zeiger auf einen Speicherbereich repräsentiert.
       

In der Tabelle 3.4 sind die wichtigsten und somit auch elementarsten .NET-Datentypen aufgeführt. Bis auf object und string sind alle der Gruppe der Wertetypen zuzurechnen. Damit stellt sich auch die Frage, warum .NET diese strikte Trennung bei den Datentypen macht. Die Antwort lautet: um die Effizienz zu steigern. Da .NET konsequent den objektorientierten Ansatz verfolgt, müssen ausnahmslos alle Daten Objekte sein. Da ein Objekt jedoch auch mit einem verwaltungstechnischen Overhead verbunden ist, werden die einfachsten Datentypen erst zur Laufzeit als Objekte betrachtet.

Initialisierung von Variablen

Deklarieren Sie Variablen im Gültigkeitsbereich einer Methode (beispielsweise Main), gilt diese zwar als bekannt, jedoch nicht mit einem bestimmten Startwert initialisiert. Sie müssen einer methodeninternen, lokalen Variablen daher ausdrücklich einen Wert zuweisen, bevor Sie zum Beispiel mit der Methode Console.WriteLine zum ersten Mal darauf lesend zugreifen. Der folgende Code führt deshalb auch zu einem Compilerfehler bei der Kompilierung:


static void Main(string[] args) 
{
  // fehlerhafter Code, ausgelöst durch eine nicht initialisierte 
  // Variable!!!
  long lngVar;
  Console.WriteLine(lngVar);
}

Um eine Variable zu initialisieren, können Sie der Variablen bei der Deklaration einen Startwert zuweisen, z.B.:


long lngVar = 0;

oder, um Ihnen schon an dieser Stelle eine andere syntaktische Variante vorzustellen, mit dem new-Operator:


long lngVar = new long();

Sollten Sie bei der Initialisierung oder während der Laufzeit versuchen, einer Variablen einen Wert zuzuweisen, der größer oder kleiner ist als der durch den Typ beschriebenen Wertebereich, erhalten Sie einen Compiler- bzw. Laufzeitfehler.

Ganzzahlige Datentypen

C# stellt acht ganzzahlige Datentypen zur Verfügung, von denen vier vorzeichenbehaftet sind, der Rest nicht. Die uns interessierenden CLS-konformen sind:

gp  Byte
gp  Int16
gp  Int32
gp  Int64

Int16, Int32 und Int64 haben einen Wertebereich, der nahezu gleichmäßig über die negative und positive Skala verteilt ist. Die vorzeichenlosen Datentypen, zu denen auch Byte gehört, decken hingegen nur den positiven Wertebereich, beginnend bei 0, ab. Der vorzeichenlose Typ Byte, der im Gegensatz zu SByte CLS-konform ist, ist insbesondere dann von Interesse, wenn auf binäre Daten zugegriffen wird.

Ganzzahlige Literale können in Dezimal- oder Hexadezimalform übergeben werden. Hexadezimale Zahlen (Basis = 16) erhalten zusätzlich das Präfix 0x. Die folgende Variable intHex beschreibt die Dezimalzahl 225:


int intHex = 0xE1;

Dezimalzahlen

Versuchen Sie einmal, die beiden folgenden Codezeilen zu kompilieren:


float fltValue = 0.123456789;
Console.WriteLine(fltValue);

Normalerweise würde man erwarten, dass der C#-Compiler daran nichts zu beanstanden hat. Dennoch zeigt er erstaunlicherweise einen Kompilierfehler an. Wie ist das zu erklären?

Auch ein Literal wie unsere Zahl 0,123456789 muss zunächst temporär in den Speicher geschrieben werden, bevor es endgültig der Variablen zugewiesen werden kann. Um eine Zahl im Speicher abzulegen, muss die Laufzeitumgebung aber eine Entscheidung treffen: Es ist die Entscheidung darüber, wie viel Speicherplatz dem Literal zugestanden wird. Das kommt aber auch der Festlegung auf einen bestimmten Datentyp gleich. Bei Dezimalzahlliteralen ist diese Festlegung immer eindeutig:


Literale, die eine Dezimalzahl beschreiben, werden von der .NET-Laufzeitumgebung als double-Typ angesehen.

Nun kommt es bei der Zuweisung unseres Literals an fltValue jedoch zu einem Problem: Das Literal ist vom Typ double, die Variable, die den Inhalt aufnehmen soll, vom Typ float. Per Definition weist double aber einen größeren Wertebereich als float auf – mit der Folge, dass unter Umständen vom Literal ein Wert beschrieben sein könnte, der größer ist als der, den ein float zu speichern vermag. Der Compiler verweigert deshalb diese Zuweisung.

Es gibt einen sehr einfachen Ausweg aus diesem Dilemma: Man hängt dazu an das Literal ein passendes Suffix an, hier F (oder gleichwertig f), mit dem wir den Typ float für das Literal erzwingen:


float fltValue = 0.123456789F;
Console.WriteLine(fltValue);

Nun ist der C#-Compiler in der Lage, den Inhalt an der Konsole anzuzeigen – vorausgesetzt, die Zahl entspricht dem Wertebereich eines float.


Tabelle 3.5   Typsuffix der Fließkommazahlen

Suffix Fließkommatyp
F oder f float
D oder d double
M oder m decimal

Die Genauigkeit von Dezimalzahlen

Die drei Typen float, double und decimal, mit denen unter C# Fließkommazahlen dargestellt werden können, beschreiben nicht nur unterschiedliche Wertebereiche, sondern auch – was im Grunde genommen noch viel wichtiger ist – unterschiedliche Genauigkeiten. Auf herkömmlichen Systemen beträgt die Genauigkeit eines float-Typs etwa zehn Stellen, der eines double-Typs etwa 16 Stellen. Abhängig ist die Genauigkeit dabei immer von der Anzahl der Ziffern des ganzzahligen Anteils der Dezimalzahl.

Das folgende Codefragment demonstriert die Genauigkeit, die mit einem float erreicht werden kann.


// --------------------------------------------------------------
// Beispiel: ...\Kapitel 3\DezimalGenauigkeit
// --------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Text;
namespace DezimalGenauigkeit
{
  class Program  {
    static void Main(string[] args) {
      float x, y;
      x = 0.123456789F;
      y = 0.1234567891F;
      // Prüfung, ob die Inhalte der Variablen x und y gleich sind
      if(x == y)
        Console.WriteLine("Beide Werte sind gleich.");
      else
        Console.WriteLine("Beide Werte sind ungleich.");
      Console.ReadLine();
    }
  }
}

Es werden zunächst zwei Variablen vom Typ float deklariert. Danach wird beiden ein Wert zugewiesen, der sich nur an der zehnten Nachkommastelle unterscheidet. Mit der Anweisung


if(x == y) 

werden die Inhalte der beiden Variablen auf Gleichheit überprüft. Entspricht der von x repräsentierte Wert dem von y, soll die Meldung »Beide Werte sind gleich« ausgegeben werden, weichen die Variableninhalte voneinander ab, sollte die Ausgabe »Beide Werte sind ungleich« lauten.

Erstaunlicherweise erscheint nach dem Start der Laufzeit die Meldung mit der Aussage, die von den beiden Variablen beschriebenen Werte seien gleich. Diese offensichtliche Falschaussage ist darauf zurückzuführen, dass ein float nicht in der Lage ist, alle im vorliegenden Fall angegebenen Nachkommastellen exakt zu interpretieren – der Typ ist schlichtweg überfordert. Für Berechnungen, die eine höhere Genauigkeit erfordern, ist ein float daher weniger gut geeignet.

Ein ähnlicher Test, diesmal mit einem double-Typ, führt zu demselben Ergebnis – allerdings tritt dieser Effekt erst auf, wenn die Anzahl der Nachkommastellen erhöht wird. Dies ist auch häufig der Grund dafür, sich für den einen oder anderen Dezimaldatentypen zu entscheiden. Die Größenordnung, die bereits ein float darstellen kann, ist per Definition bereits so groß, dass hier weniger das Entscheidungskriterium zu suchen ist.

Wenn von der Genauigkeit von Fließkommazahlen gesprochen wird, bedeutet das nicht, dass die Genauigkeit auf den Dezimalteil bezogen wird. Vielmehr ist es die Genauigkeit, mit der beispielsweise beim float die ersten zehn Zahlen – beginnend links mit der ersten – unterschieden werden können. Um dies zu testen, brauchen Sie nur die beiden Literale des vorhergehenden Beispiels zu ändern, z.B.:


static void Main(string[] args) {
  float x, y;
  x = 10.123456F;
  y = 10.1234567891F;
  if(x == y)
    Console.WriteLine("Beide Werte sind gleich.");
  else
    Console.WriteLine("Beide Werte sind ungleich.");
  Console.Read();
}

Wenn Sie diesen Code laufen lassen, werden Sie die folgende Ausgabe erhalten:


Beide Werte sind ungleich.

Hängen Sie die Zahl 7 als letzte Nachkommastelle an die Variable x an, also


x = 10.1234567

sieht der Compiler beide Variableninhalte als gleich an. Er berücksichtigt demnach die achte Nachkommastelle nicht mehr für den Vergleich. Zum Vergleich: Im Beispiel mit einem einziffrigen ganzzahligen Anteil war es die zehnte Nachkommastelle.

Die Forderung nach sehr hoher Genauigkeit einer Dezimalzahl können die beiden Datentypen float und double manchmal nicht ausreichend erfüllen. Bei noch höheren Ansprüchen muss die Wahl auf einen deutlich präziseren Datentypen fallen: decimal. Damit lassen sich Zahlen darstellen, die eine Genauigkeit bis zu 28 Nachkommastellen aufweisen.

Während die Zuweisung eines ganzzahligen Literals an eine decimal-Variable in bekannter Art und Weise erfolgt, also beispielsweise mit

decimal decA = 120;

muss bei der Zuweisung einer nummerischen Zahl mit hohem Dezimalanteil wieder ein kleiner Trick angewendet werden. Wenn Sie im Texteditor beispielsweise


decA = 0.1234567890123456789012;

eingeben, erhalten wir bei der Kompilierung einen Compilerfehler. Nach den Gesetzen der Typkonvertierung, die wir in Abschnitt 3.3.6 behandeln werden, kann eine Zahl vom Typ int einem decimal zugewiesen werden, ohne dass es zu einem Datenverlust kommt. Der int wird dabei implizit in decimal umgewandelt. Dieselbe Aussage gilt allerdings nicht, wenn ein double einem decimal zugewiesen werden soll – es kommt zu einer Fehlermeldung in der Entwicklungsumgebung, da der Compiler ein Dezimalzahlliteral als double-Typ ansieht. Dieses Verhalten kann wieder durch ein Suffix, hier der Buchstabe »M« bzw. »m«, vermieden werden:


decA = 0.1234567890123456789012M;

Diese Erkenntnis wollen wir nun benutzen, um zum Abschluss die Genauigkeit zu testen, mit der eine Zahl vom Typ decimal zu arbeiten in der Lage ist. Dazu dient uns der folgende Code.


// --------------------------------------------------------------
// Beispiel: ...\Kapitel 3\DecimalVergleich
// --------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Text;
namespace DecimalVergleich
{
  class Program {
    static void Main(string[] args) {
      decimal x, y, z; 
      x = 0.1234567890123456789012345678M;
      y = 0.1234567890123456789012345678M;
      z = 0.1234567890123456789012345679M;
      // Vergleich der Variablen x und y
      if(x == y) 
        Console.WriteLine("x und y sind gleich.");
      else
        Console.WriteLine("x und y sind ungleich.");
      // Vergleich der Variablen x und z
      if(x == z)
        Console.WriteLine("x und z sind gleich.");
      else
        Console.WriteLine("x und z sind ungleich.");
      Console.ReadLine();
    }
  }
}

Beachten Sie, dass die beiden Variablen x und y identisch sind, während sich x und z in der 28. Nachkommastelle unterscheiden. Lassen Sie das Programm laufen, wird an der Konsole folgende Ausgabe angezeigt:


x und y sind gleich.
x und z sind ungleich.

Die Laufzeit erkennt den Unterschied an der 28. Nachkommastelle. Das ist natürlich eine deutliche Steigerung gegenüber float und double.

Wird der ganzzahlige Anteil eines decimal vergrößert, z.B. in


decA = 1000.1234567890123456789012346D

bewirkt jede weitere Ziffer links vom Komma einen Verlust an Genauigkeit, im Beispiel von decA weist der Dezimalteil nur noch eine Genauigkeit von 25 Stellen rechts vom Komma auf.

Zeichenbasierte Datentypen

Variablen vom Typ char können ein Zeichen des Unicode-Zeichensatzes aufnehmen. Unicode ist die Erweiterung des ein Byte großen ASCII- bzw. ANSI-Zeichensatzes mit seinen insgesamt 256 verschiedenen Zeichen. Unicode berücksichtigt die Bedürfnisse außereuropäischer Zeichensätze, für die eine Ein-Byte-Codierung nicht ausreichend ist. Jedes Unicode-Zeichen beansprucht zwei Byte, folglich ist der Unicode-Zeichensatz auch auf 65.536 Zeichen beschränkt. Die ersten 128 Zeichen (0–127) entsprechen denen des ASCII-Zeichensatzes, die folgenden 128 Zeichen beinhalten unter anderem Sonderzeichen und Währungssymbole.

Literale, die dem Typ char zugewiesen werden, werden in einfache Anführungsstriche gesetzt, z.B.:


char chrZeichen = 'A';

Um den ASCII-Wert eines einzelnen Zeichens zu erhalten, braucht man nur den Typ char einem Zahlentyp wie beispielsweise int, long oder float zuzuweisen.


char chrZeichen = 'A';
int intASCII = chrZeichen;
Console.WriteLine(intASCII); // die Ausgabe lautet 65

Die implizite Umwandlung eines char in einen Zahlenwert macht anscheinend keine Probleme, der umgekehrte Weg, die Umwandlung eines Zahlenwerts in einen char, ist allerdings nicht möglich.

char beschränkt sich nur auf ein Zeichen. Um eine Zeichenkette, die sich aus keinem oder bis zu maximal ca. 231  Einzelzeichen zusammensetzt, zu speichern oder zu bearbeiten, deklarieren Sie eine Variable vom Datentyp string. Die Einzelzeichen werden dabei wie bei char als Unicode-Zeichen der Größe 16 Bit behandelt. Zeichenketten werden grundsätzlich in doppelte Anführungsstriche gesetzt.


string str = "C# ist spitze."

Weitere Datentypen

Variablen vom Typ bool (Boolean) können nur zwei Zustände beschreiben, nämlich true oder false, z.B.:


bool myBol = true;

false ist der Standardwert.


Hinweis   In vielen Programmiersprachen wird false nummerisch mit 0 beschrieben, true durch alle Werte, die von 0 abweichen. .NET ist hier sehr viel strenger. Hier ist true nicht 1 und auch nicht 67, sondern ganz schlicht true. Aus diesem Grund ist auch die folgende Anweisung falsch, die so in anderen Programmiersprachen durchaus möglich ist: bool myBool = 2; Das hat natürlich auch Auswirkungen auf Bedingungsprüfungen, wie Sie später noch sehen werden.

Der allgemeinste aller Datentypen ist object und beschreibt in seinen vier Byte einen Zeiger auf die Speicheradresse eines Objekts. Eine Variable dieses Typs kann jeden beliebigen anderen Datentypen beschreiben: Ob es sich um eine Zahl, eine Zeichenfolge, eine Datenbankverbindung oder um ein anderes Objekt wie zum Beispiel um die Schaltfläche in einem Windows-Fenster handelt, spielt dabei keine Rolle. Zur Laufzeit wird eine auf object basierende Variable passend aufgelöst und die gewünschte Operation darauf ausgeführt.

Um das zu demonstrieren, ist im folgenden Codefragment eine Variable vom Typ object deklariert, der zuerst ein Zahlenliteral und anschließend eine Zeichenfolge zugewiesen wird:


object objUniversal;
objUniversal = 5;
Console.WriteLine(objUniversal);
objUniversal = "Hallo Welt.";
Console.WriteLine(objUniversal);

Die Variable objUniversal schluckt beide Zuweisungen anstandslos – an der Konsole wird zuerst die Zahl 5 und danach die Zeichenfolge angezeigt.

Ganz gleich, was eine Variable vom Typ object beinhaltet, sie enthält grundsätzlich immer nur einen Zeiger (auch als Verweis oder Referenz bezeichnet) auf ein Objekt – selbst dann, wenn es sich um eine Zahl handelt. Herkömmliche Betriebssysteme beschreiben 32-Bit-Speicheradressen. Daraus folgt auch die einheitliche Größe dieses Datentyps von vier Byte – unabhängig davon, welcher Typ referenziert wird.

Die einfachen Datentypen als Objekte

Eine Variable zu deklarieren, sieht harmlos und unscheinbar aus. Und dennoch, hinter dem Variablennamen verbergen sich Möglichkeiten, die Sie bisher vermutlich noch nicht erahnen. In der .NET-Laufzeitumgebung wird alles aus der objektorientierten Brille betrachtet – sogar die einfachen Datentypen.

Ein simpler Short soll ein Objekt sein? Wenn Sie dieser Aussage keinen Glauben schenken wollen, schreiben Sie folgende Codezeile:


Int16.

Beachten Sie bitte hierbei den Punkt, der auf Int16 folgt. Sie werden feststellen, dass hinter der Punktangabe eine Liste aufgeklappt wird, die IntelliSense-Unterstützung.

In dieser Liste sind alle Eigenschaften und Methoden aufgeführt, die ein Objekt vom Typ Int16 auszeichnen. Sie können aus dem Angebot auswählen, wenn Sie mit den Steuerungstasten zu der gewünschten Funktionalität navigieren und dann die (ÿ)-Taste drücken. Der ausgewählte Eintrag aus IntelliSense wird sofort vom Code übernommen, was den Vorteil hat, dass ein Schreibfehler ausgeschlossen ist.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 3.4   IntelliSense-Unterstützung in der Entwicklungsumgebung

Wenn Sie beispielsweise wissen wollen, wo die wertmäßige Ober- bzw. Untergrenze des Int16-Typs liegt, könnten Sie dies mit dem folgenden Codefragment abfragen:


Console.WriteLine("Int16(min) = {0}", Int16.MinValue);
Console.WriteLine("Int16(max) = {0}", Int16.MaxValue);

An der Konsole erfolgt danach die Anzeige:


Int16(min) = –32768
Int16(max) = 32767

Wahrscheinlich werden Sie schon festgestellt haben, dass IntelliSense nicht nur im Zusammenhang mit der Punktnotation funktioniert. Sobald Sie in einer Codezeile den ersten Buchstaben eintippen, wird IntelliSense geöffnet und bietet Ihnen alle programmierbaren Optionen an, die mit dem eingegebenen Buchstaben zugreifbar sind. Die Auswahl erfolgt analog wie oben beschrieben.


Galileo Computing

3.3.6 Typkonvertierung  toptop

Sehen wir uns die folgenden beiden Anweisungen an:


int intVar = 12000;
long lngVar = intVar;

Es wird die Variable intVar vom Typ int deklariert und ihr ein Wert zugewiesen. Im zweiten Schritt erfolgt wiederum eine Variablendeklaration, diesmal vom Typ long. Der Inhalt der zuvor deklarierten Variablen intVar wird lngVar zugewiesen. Der C#-Compiler wird beide Anweisungen anstandslos kompilieren.

Nun ändern wir die Reihenfolge ab, deklarieren zuerst die long-Variable, weisen ihr den Wert von 12000 zu und versuchen dann, lngVar der int-Variablen zuzuweisen:


long lngVar = 12000;
int intVar = lngVar;

Diesmal ist das Ergebnis nicht wie vielleicht erwartet – der C#-Compiler quittiert die Zuweisung des long-Typs an den int-Typ mit einer Fehlermeldung, obwohl der Wertebereich eines int die Zuweisung von 12000 eindeutig verkraftet.

Das auftretende Problem beruht auf einer einengenden Datentypumwandlung: Der Wertebereich eines int ist kleiner als der eines long. Im Gegensatz dazu ist die Zuweisung eines int an einen long eine aufweitende Operation, weil der long einen größeren Wertebereich als int hat.

Immer dann, wenn bei einer Operation zwei unterschiedliche Datentypen im Spiel sind, muss der Typ, der rechts vom Zuweisungsoperator steht, in den Typ umgewandelt werden, der sich auf der linken Seite befindet. Man spricht hierbei auch von der Konvertierung.

Prinzipiell werden zwei Arten der Konvertierung unterschieden:

gp  die implizite Konvertierung
gp  die explizite Konvertierung

Die implizite Konvertierung

Eine implizite Konvertierung nimmt der C#-Compiler selbst vor. Dies setzt eine aufweitende Zuweisungsoperation voraus. Am besten schauen wir uns dazu die Abbildung 3.5 an.

Die Pfeilrichtung gibt eine aufweitende, also implizite Konvertierung vor, entgegengesetzt der Pfeilrichtung wäre eine Konvertierung einengend. Demzufolge wird ein byte anstandslos implizit in einen short, int, long usw. konvertiert, aber nicht umgekehrt beispielsweise ein int in byte. Beachten Sie insbesondere, dass es keine impliziten Konvertierungen zwischen den Gleitkommatypen float/double und decimal gibt.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 3.5   Die implizite Konvertierung einfacher Datentypen

Eine besondere Stellung nehmen bool, string, char und object ein. Mit einem bool oder einem string sind keine impliziten Konvertierungen möglich, ein char kann mit Ausnahme von byte und short jedem anderen Typ zugewiesen werden. Variablen vom Typ object wiederum unterliegen Gesichtspunkten, die wir erst ab Kapitel 4 erörtern.

Unter Berücksichtigung der zuvor beschriebenen Gesetzmäßigkeiten sind die folgenden impliziten Konvertierungen möglich:


// zulässige implizite Konvertierungen
int intVar = 15;
decimal decVar = intVar;
char chrVar = 'K';
int intValue = chrVar;
short shtVar = 11;
double dblVar = shtVar;

Die folgenden drei Versuche werden hingegen vom C#-Compiler abgelehnt:


// unzulässige implizite Konvertierungen
float fltVar = 3.12F;
decimal decVar = fltVar;
byte bytVar = 20;
char c = bytVar;
int iVar = 1;
bool bolVar = iVar;

Die explizite Konvertierung

Unter expliziter Konvertierung wird die ausdrückliche Anweisung an den Compiler verstanden, den Wert eines bestimmten Datentyps in einen anderen umzuwandeln. Explizite Konvertierung folgt einer sehr einfachen Syntax: Vor dem zu konvertierenden Ausdruck wird in runden Klammern der Typ angegeben, in den die Konvertierung erfolgen soll, also:


// Syntax: explizite Konvertierung
(Zieldadentyp)Ausdruck

Man spricht bei den so eingesetzten runden Klammern auch vom Typkonvertierungsoperator.

Mit der expliziten Konvertierung wären die folgenden beiden Zuweisungen möglich, die weiter oben noch einen Kompilierfehler verursacht haben:


float fltVar = 3.12F;
decimal decVar = (decimal)fltVar;
byte bytVar = 20;
char c = (char)bytVar;

Obwohl die explizite Konvertierung weitere Möglichkeiten eröffnet, sind ihr naturgemäß auch Grenzen gesetzt, da nur dann in einen anderen Typ umgewandelt werden kann, wenn der Zieldatentyp in Beziehung zum Ursprungsdatentyp steht (siehe Abbildung 3.5). Beispielsweise bleibt ein boolescher Wert unter .NET immer ein boolescher Wert. Damit ist die folgende Konvertierung unter C# falsch, obwohl sie in anderen Programmiersprachen durchaus zulässig ist:


int iVar = 1;
// fehlerbehaftete explizite Konvertierung
bool bolVar = (bool)iVar;

Sehr ähnlich, jedoch einfacher zu verstehen ist, dass der Konvertierungsversuch eines string in einen int jeglicher Logik entbehrt:


string strText = "Hallo";
int intVar = strText; // FALSCH!!

Explizite Konvertierung mit den Methoden der Klasse »Convert«

Die explizite Konvertierung mit dem Typkonvertierungsoperator ist eine Möglichkeit, einen Datentyp zu erzwingen. Eine zweite bietet die .NET-Klassenbibliothek. Es handelt sich hierbei um die Klasse Convert im Namespace System, die eine Reihe von Methoden für diesen Zweck bereitstellt.


Tabelle 3.6   Die Konvertierungsmethoden der Klasse »Convert«

Methode Beschreibung
ToBoolean(Ausdruck) Konvertiert den Ausdruck in einen bool-Typ.
ToByte(Ausdruck) Konvertiert den Ausdruck in einen byte-Typ.
ToChar(Ausdruck) Konvertiert den Ausdruck in einen char-Typ.
ToDecimal(Ausdruck) Konvertiert den Ausdruck in einen decimal-Typ.
ToDouble(Ausdruck) Konvertiert den Ausdruck in einen double-Typ.
ToInt16(Ausdruck) Konvertiert den Ausdruck in einen short-Typ.
ToInt32(Ausdruck) Konvertiert den Ausdruck in einen int-Typ.
ToInt64(Ausdruck) Konvertiert den Ausdruck in einen long-Typ.
ToSByte(Ausdruck) Konvertiert den Ausdruck in einen sbyte-Typ.
ToSingle(Ausdruck) Konvertiert den Ausdruck in einen float.
ToString(Ausdruck) Konvertiert den Ausdruck in einen string.
ToUInt16(Ausdruck) Konvertiert den Ausdruck in einen ushort.
ToUInt32(Ausdruck) Konvertiert den Ausdruck in einen uint.
ToUInt64(Ausdruck) Konvertiert den Ausdruck in einen ulong.

Damit ist das Codefragment


long lngVar = 4711;
int intVar = (int)lngVar;

gleichwertig mit


long lngVar = 4711;
int intVar = Convert.ToInt32(lngVar);

In zwei ganz wesentlichen Punkten unterscheidet sich die Konvertierung mit den Methoden der Convert-Klasse von der mit dem Konvertierungsoperator:

1.  Grundsätzlich werden alle Konvertierungen mit den Methoden der Convert-Klasse auf einen eventuellen Überlauf hin untersucht.
2. Es können Konvertierungen durchgeführt werden, die mit dem Typkonvertierungsoperator unzulässig sind.
       

Den erstgenannten Punkt werden wir im folgenden Abschnitt behandeln, während wir uns an dieser Stelle zunächst dem zweiten zuwenden. Angenommen, wir wollen an der Eingabeaufforderung die Eingabe in einer Integervariablen speichern, muss die Anweisung dazu lauten:


int intDigit = Convert.ToInt32(Console.ReadLine());

Bekannterweise liefert ReadLine die Benutzereingabe als Zeichenfolge vom Typ string zurück. Wäre die Methode Convert.ToInt32 gleichwertig mit dem Typkonvertierungsoperator, würde der C#-Compiler auch die folgende Anweisung anstandslos kompilieren:


int intDigit = (int)Console.ReadLine(); // FALSCH!!

Allerdings wird uns der Compiler diese Anweisung mit der Fehlermeldung


Konvertierung des Typs 'string' zu 'int' nicht möglich

quittieren, denn eine explizite Konvertierung des Typs string in einen nummerischen Typ mit dem Typkonvertierungsoperator ist auch dann unzulässig, wenn die Zeichenfolge eine Zahl beschreibt. string und int stehen nach Aussage von Abbildung 3.5 in keinerlei Beziehung zueinander. Die Methoden der Klasse Convert sind aber so ausgebildet, dass in diesen Fällen dennoch eine Konvertierung erfolgt.

Wie wir gesehen haben, können sich die Methoden der Convert-Klasse über diese Gesetzmäßigkeit hinwegsetzen, natürlich vorausgesetzt, dass die Konvertierung aus logischer Sicht sinnvoll ist. Solange aber eine Zeichenfolge eine Zahl beschreibt, darf auch eine Zeichenfolge durchaus in einen nummerischen Typ überführt werden.

Bereichsüberschreitung infolge expliziter Konvertierung

Eine explizite Konvertierung lässt eine typeinengende Umwandlung zu. Damit drängt sich sofort eine Frage auf: Was passiert, wenn der Wert des Ausgangsausdrucks größer ist als der Maximalwert des Typs, in den konvertiert wird? Nehmen wir dazu beispielsweise an, wir hätten eine Variable vom Typ short deklariert und ihr den Wert 436 zugewiesen. Nun soll diese Variable in den Typ byte überführt werden, der den Wertebereich zwischen 0–255 beschreibt.


short shtVar = 436;
byte byteVar = (byte)shtVar;
Console.WriteLine(byteVar);

Dieser Code resultiert in der Ausgabe:


180

Um zu verstehen, wie es zu dieser zunächst unverständlichen Ausgabe kommt, müssen wir uns die bitweise Darstellung der Zahlen ansehen. Für den Inhalt der Variablen shtVar ist dies:


436 = 0000 0001 1011 0100

Nach der Konvertierung liegt das Ergebnis 180 vor, beschrieben durch:


180 = 1011 0100

Vergleichen wir jetzt die bitweise Darstellung der beiden Zahlen, kommen wir sehr schnell zu der Erkenntnis, dass bei einer expliziten Konvertierung mit dem Typkonvertierungsoperator bei Überschreiten der Bereichsgrenze des Zieldatentyps die überschüssigen Bits einfach ignoriert werden. Aus dem verbleibenden Rest wird schließlich die neue Zahl gebildet.

Dieses Verhalten kann zu sehr schwer zu lokalisierenden, ernsthaften Fehlern in einer laufenden Anwendung führen. Wenn Sie in einer Anwendung Code entwickeln und explizit konvertieren müssen, sollten Sie daher Kontrolle über einen eventuell eintretenden Überlauf haben. Unter C# gibt es dazu drei Alternativen:

1.  die Operatoren checked und unchecked
2. die Einstellung im Projekteigenschaftsfenster
       
3. Verzicht auf den Typkonvertierungsoperator und stattdessen eine Methode der Klasse Convert benutzen, die zur Auslösung einer Fehlermeldung führt
       

Die Operatoren »checked« und »unchecked«

Wenden wir uns den unter Punkt 1 genannten Schlüsselwörtern checked und unchecked zu, und schauen wir uns an einem Beispiel den Einsatz und die Wirkungsweise an:


// --------------------------------------------------------------
// Beispiel: ...\Kapitel 3\CheckedDemo
// --------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Text;
namespace CheckedDemo
{
  class Program {
    static void Main(string[] args) {
      // Zahleneingabe anfordern
      Console.Write("Geben Sie eine Zahl im Bereich von ");
      Console.Write("0...{0} ein: ", Int16.MaxValue);
      // Eingabe einem short-Typ zuweisen
      short shtVar = Convert.ToInt16(Console.ReadLine());
      // Überlaufüberprüfung einschalten
      byte byteVar = checked((byte)shtVar);
      Console.WriteLine(byteVar);
      Console.ReadLine();
    }
  }
}

Nach dem Starten der Anwendung wird der Benutzer dazu aufgefordert, eine Zahl im Bereich von 0 bis zum Maximalwert eines short einzugeben. Entgegengenommen wird die Eingabe durch die Methode Console.ReadLine, die ihrerseits die Eingabe als Zeichenfolge, also vom Typ string zurückliefert. Um die gewünschte Zahl einer short-Variablen zuweisen zu können, muss explizit konvertiert werden. Beachten Sie bitte, dass wir dazu die Methode ToInt16 der Klasse Convert einsetzen müssen, da eine Konvertierung eines string in einen short mit dem Typkonvertierungsoperator nicht zulässig ist:

short shtVar = Convert.ToInt16(Console.ReadLine());

Gibt der Anwender eine Zahl ein, die den Wertebereich des short-Typs überschreitet, wird ein Laufzeitfehler ausgelöst und die Laufzeit der Anwendung beendet. Falls der Wertebereich nicht überschritten wird, wird die dann folgende Anweisung ausgeführt:


byte byteVar = checked((byte)shtVar);

In dieser Anweisung steckt allerdings eine Gemeinheit, denn nun soll der Inhalt der short-Variablen einer byte-Variablen zugewiesen werden. Je nachdem, welche Zahl der Anwender eingegeben hat, wird die Zuweisung fehlerfrei erfolgen oder, bedingt durch die Überprüfung mit checked, zu einem Fehler führen. Löschen Sie checked aus dem Programmcode, wird die Zuweisung einer Zahl, die den Wertebereich eines byte-Typs überschreitet, in keinem Fall einen Fehler verursachen.

checked ist ein Operator und wird verwendet, um einen eventuell auftretenden arithmetischen Überlauf zu steuern. Die allgemeine Syntax hierzu lautet:


// Syntax: Der checked-Operator
checked(Ausdruck);

Tritt zur Laufzeit ein Überlauf ein, weil der Anwender eine Zahl eingegeben hat, die den Wertebereich des Typs überschreitet, in den konvertiert werden soll, wird ein Laufzeitfehler ausgelöst, der unter .NET auch als Ausnahme bzw. Exception bezeichnet wird. Geben wir beispielsweise an der Konsole die Zahl 436 ein, werden wir die folgende Mitteilung erhalten:

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 3.6   Fehlermeldung durch Überlauf

Nach dem Schließen der Fehlermeldung wird die unplanmäßig Anwendung beendet. Nun könnten Sie argumentieren, dass das Beenden der Laufzeit auch nicht das sein kann, was unbedingt erstrebenswert ist. Dieses Argument ist vollkommen richtig, aber Laufzeitfehler lassen sich mittels Programmcode abfangen, und die Anwendung bleibt danach in einem ordnungsgemäßen Laufzeitzustand. Diesem Thema werden wir uns in Kapitel 9 dieses Buches noch ausgiebig widmen.

Falls nicht nur ein einzelner Ausdruck, sondern mehrere Ausdrücke innerhalb eines Anweisungsblocks auf einen möglichen Überlauf hin kontrolliert werden sollen, können Sie hinter checked einen Anweisungsblock angeben, innerhalb dessen der unkontrollierte Überlauf durch die Auslösung eines Laufzeitfehlers unterbunden wird.


checked {/*...*/}

Wie diese Variante von checked eingesetzt wird, können Sie dem nachfolgendem Beispiel entnehmen.


static void Main(string[] args) 
{
  checked 
  {
    short shtVar = 436;
    int intVar = 1236555;
    byte byteVar = (byte)shtVar;
    shtVar = (short)intVar;
    Console.WriteLine(byteVar);
    Console.ReadLine();
  }

Wir können festhalten, mit checked eine gewisse Kontrolle ausüben zu können, falls zur Laufzeit bedingt durch die explizite Konvertierung ein Überlauf eintreten kann. Der Operator unchecked ist die Umkehrung der Arbeitsweise von checked, er schaltet die Überprüfung des Überlaufs aus und ist der Standard.

Während checked sich nur lokal auf den in runden Klammern stehenden Ausdruck bzw. einen eingeschlossenen Anweisungsblock bezieht, kann durch eine Änderung im Projekteigenschaftsfenster die Kontrolle über sämtliche auftretenden Überläufe in einer Anwendung ausgeübt werden. Öffnen Sie dieses Fenster, indem Sie im Projektmappen-Explorer das Projekt markieren, dessen Kontextmenü mit der rechten Maustaste öffnen und dann Eigenschaften wählen.

Das Projekteigenschaftsfenster wird als zusätzliche Lasche im Codeeditor angezeigt. Am linken Rand werden mehrere Auswahloptionen angeboten. Unser Problem betreffend müssen Sie sich für Erstellen entscheiden (siehe Abbildung 3.7).

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 3.7   Das Projekteigenschaftsfenster

Rechts unten sehen Sie die Schaltfläche, die mit Erweitert... beschriftet ist. Darüber wird ein Dialog geöffnet, der die gesuchte Option anbietet: